#include "first_app.hpp"
#include <iostream>
#include <stdexcept>
#include <array>
#include <vector>
#include <cassert>
#include <chrono>
#include <random>
#include <limits>
#include <numeric>
#include <algorithm>
#include <cmath>

#include "njm_utils.hpp"
#include "keyboard_movement_controller.hpp"
#include "njm_camera.hpp"
#include "njm_buffer.hpp"
#include "njm_descriptor.hpp"
#include "njm_frame_info.hpp"
#include "systems/simple_render_system.hpp"
#include "systems/point_light_system.hpp"

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include "ecs_app.hpp"

namespace njm
{
    
    FirstApp::FirstApp()
    {
        globalPool = NjmDescriptorPool::Builder(njmDevice)
            .setMaxSets(NjmSwapChain::MAX_FRAMES_IN_FLIGHT)
            .addPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, NjmSwapChain::MAX_FRAMES_IN_FLIGHT)
            .addPoolSize(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, NjmSwapChain::MAX_FRAMES_IN_FLIGHT)
            .build();
        loadGameObjs();
        loadImgs();
        printAssetsInfo();
        
    }
    FirstApp::~FirstApp()
    {
    }

    void FirstApp::run()
    {
        std::cout << "--FirstApp::run--" << std::endl;
        // auto minOffsetAlignment = std::lcm(
        //     njmDevice.getPhysicalDeviceProperties().limits.minUniformBufferOffsetAlignment,
        //     njmDevice.getPhysicalDeviceProperties().limits.nonCoherentAtomSize);
        auto minOffsetAlignment = 1;
        std::vector<std::unique_ptr<NjmBuffer>> uboBuffers(NjmSwapChain::MAX_FRAMES_IN_FLIGHT);
        for (int i = 0; i < NjmSwapChain::MAX_FRAMES_IN_FLIGHT; i++)
        {
            uboBuffers[i] = std::make_unique<NjmBuffer>(
                njmDevice,
                sizeof(GlobalUbo),
                1,
                VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                minOffsetAlignment
            ); // alignment also can be default value 1.
            uboBuffers[i]->map();
        }

        // create textures
        std::vector<std::unique_ptr<NjmTexture>> texs(NjmSwapChain::MAX_FRAMES_IN_FLIGHT);
        std::vector<TextureImgBinding> texImgBindings{
            {"tex1", "eg1.png"},
        };
        TextureImgBinding texImgBinding = texImgBindings[0];
        for (int i = 0; i < NjmSwapChain::MAX_FRAMES_IN_FLIGHT; i++)
        {
            auto &imgName = texImgBinding.imgName;
            auto &texName = texImgBinding.texName;
            assert(njmImgs.find(imgName) != njmImgs.end() && ("img not found : " + imgName).c_str());
            texs[i] = std::make_unique<NjmTexture>(njmDevice, texName, njmImgs.at(imgName));
        }

        // create descriptor set layouts and descriptor sets
        auto globalSetLayout = NjmDescriptorSetLayout::Builder(njmDevice)
            .addBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_ALL_GRAPHICS)
            .addBinding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT)
            .build();
        std::vector<VkDescriptorSet> globalDescriptorSets(NjmSwapChain::MAX_FRAMES_IN_FLIGHT);

        // update descriptor sets
        for (int i = 0; i < NjmSwapChain::MAX_FRAMES_IN_FLIGHT; i++)
        {
            auto bufferInfo = uboBuffers[i]->descriptorInfo();
            auto combinedImageInfo = texs[i]->getCombinedDescriptor();

            NjmDescriptorWriter(*globalSetLayout, *globalPool)
                .writeBuffer(0, &bufferInfo)
                .writeImage(1, &combinedImageInfo)
                .build(globalDescriptorSets[i]);
        }

        std::string SDR_BASE = "src/shaders/";
        std::string vertShaderPath = SDR_BASE + "simple_shader_vert.spv";
        std::string fragShaderPath = SDR_BASE + "simple_shader_frag.spv";
        // std::string vertShaderPath = SDR_BASE + "simple_shader.vert.spv";
        // std::string fragShaderPath = SDR_BASE + "simple_shader.frag.spv";
        SimpleRenderSystem renderSys{njmDevice, njmRenderer.getSwapChainRenderPass(), globalSetLayout->getDescriptorSetLayout(),
            vertShaderPath, fragShaderPath};
        PointLightSystem pointLightSys{njmDevice, njmRenderer.getSwapChainRenderPass(), globalSetLayout->getDescriptorSetLayout()};
        
        KeyboardMovementController cameraController{};
        NjmGameObj viewerObject = NjmGameObj::createGameObj();
        viewerObject.transform.translation.z = -2.5f;
        NjmCamera camera{};

        auto currentTime = std::chrono::high_resolution_clock::now();
        while (!njmWindow.shouldClose())
        {
            std::cout << "--while (!njmWindow.shouldClose()) looping-- " << std::endl;
            glfwPollEvents();
            auto newTime = std::chrono::high_resolution_clock::now();
            float frameTime = std::chrono::duration<float, std::chrono::seconds::period>(newTime - currentTime).count();
            currentTime = newTime;
            frameTime = glm::min(frameTime, MAX_FRAME_TIME);
            // frametime is dt.
            cameraController.moveInPlaneXZ(njmWindow.getGLFWwindow(), frameTime, viewerObject);
            // camera.setViewDirection(glm::vec3(0.f), glm::vec3{0.5f, 0.5f, 0.5f});
            // camera.setViewTarget(glm::vec3{-1.f, -2.f, 2.f}, glm::vec3{0.f, 0.f, 2.5f});
            camera.setViewYXZ(viewerObject.transform.translation, viewerObject.transform.rotation);
            float aspect = njmRenderer.getAspectRatio();
            // camera.setOrthographicProjection(-aspect, aspect, -1.f, 1.f, -1.f, 1.f);
            camera.setPerspectiveProjection(glm::radians(50.f), aspect, 0.1f, 100.f);

            if (auto cmdBuf = njmRenderer.beginFrame())
            {
                int frameIndex = njmRenderer.getFrameIndex();
                FrameInfo frameInfo{
                    frameIndex, frameTime, 
                    cmdBuf, camera, 
                    globalDescriptorSets[frameIndex], gameObjs};

                // update ubo data
                GlobalUbo ubo{};
                ubo.view = camera.getView();
                ubo.projection = camera.getProjection();
                ubo.inverseView = camera.getInverseView();
                pointLightSys.update(frameInfo, ubo);

                uboBuffers[frameIndex]->writeToBuffer(&ubo);
                uboBuffers[frameIndex]->flush();

                // render
                njmRenderer.beginSwapChainRenderPass(cmdBuf);
                renderSys.renderGameObjs(frameInfo);
                pointLightSys.render(frameInfo);

                njmRenderer.endSwapChainRenderPass(cmdBuf);
                njmRenderer.endFrame();
            }
        }
        vkDeviceWaitIdle(njmDevice.device());
    }

    // model and gameobj
    std::unique_ptr<NjmModel> createCubeModel(NjmDevice &device, glm::vec3 offset)
    {
        NjmModel::Builder builder{};
        // builder.loadModelFromFile(device, "models/cube.obj");
        // auto cubeModel = std::make_unique<NjmModel>(device, builder);
        // return cubeModel;

        // 立方体顶点数据（8个顶点）
        std::vector<NjmModel::Vertex> cubeVertices = {
            // Front face
            {{-0.5f, -0.5f, 0.5f}, {1.0f, 0.0f, 0.0f}}, // 红色
            {{0.5f, -0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},  // 绿色
            {{-0.5f, 0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}},  // 蓝色
            {{0.5f, 0.5f, 0.5f}, {1.0f, 1.0f, 0.0f}},   // 黄色
            // Back face
            {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 1.0f}}, // 紫色
            {{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 1.0f}},  // 青色
            {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}},  // 白色
            {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 0.0f}}    // 黑色
        };
        // 立方体索引数据（36个索引，6个面×2三角形×3顶点）
        std::vector<uint32_t> cubeIndices = {
            0, 1, 2, 2, 1, 3, // 前面
            4, 5, 6, 6, 5, 7, // 后面
            0, 2, 4, 4, 2, 6, // 左面
            1, 3, 5, 5, 3, 7, // 右面
            0, 1, 4, 4, 1, 5, // 底面
            2, 3, 6, 6, 3, 7  // 顶面
        };

        builder.vertices = cubeVertices;
        builder.indices = cubeIndices;
        for (auto &v : builder.vertices)
        {
            v.position += offset;
        }
        return std::make_unique<NjmModel>(device, builder);
    }

    void FirstApp::loadGameObjs()
    {
        std::cout << "--FirstApp::loadGameObjs--" << std::endl;
        std::shared_ptr<NjmModel> cubeModel = createCubeModel(njmDevice, {0.0f, 0.0f, 0.0f});
        auto cube = NjmGameObj::createGameObj();
        cube.model = cubeModel;
        cube.color = {0.1f, 0.8f, 0.1f};
        cube.transform.translation = {.0f, .0f, .0f};
        cube.transform.scale = {.2f, .2f, .2f};
        cube.transform.rotation = {0.5f, 0.0f, 0.0f};
        gameObjs.emplace(cube.getId(), std::move(cube));

        std::string modelPath = "src/models/md1/md1.obj";
        std::string absoluteModelPath = njm::getCompletePath(modelPath);
        std::shared_ptr<NjmModel> objModel = NjmModel::createModelFromFile(njmDevice, modelPath);
        auto gameObj1 = NjmGameObj::createGameObj();
        gameObj1.model = objModel;
        gameObj1.color = {0.1f, 0.8f, 0.1f};
        gameObj1.transform.translation = {.0f, .0f, 3.0f};
        gameObj1.transform.scale = {1.f, 1.f, 1.f};
        // gameObj.transform.rotation = {0.5f, 0.0f, 0.0f};
        gameObjs.emplace(gameObj1.getId(), std::move(gameObj1));

        modelPath = "src/models/quad/quad.obj";
        absoluteModelPath = njm::getCompletePath(modelPath);
        objModel = NjmModel::createModelFromFile(njmDevice, modelPath);
        auto gameObj2 = NjmGameObj::createGameObj();
        gameObj2.model = objModel;
        gameObj2.color = {0.1f, 0.8f, 0.1f};
        gameObj2.transform.translation = {.0f, .5f, .0f};
        gameObj2.transform.scale = {1.5f, 1.5f, 1.5f};
        // gameObj.transform.rotation = {0.5f, 0.0f, 0.0f};
        gameObjs.emplace(gameObj2.getId(), std::move(gameObj2));

        std::vector<glm::vec3> lightcolors{
            {0.1f, 0.8f, 0.1f},
            {0.8f, 0.1f, 0.1f},
            {0.1f, 0.1f, 0.8f},
            {0.8f, 0.8f, 0.1f},
            {0.8f, 0.1f, 0.8f},
            {0.1f, 0.8f, 0.8f},
        };
        float lineLen = 1.5f;
        float step = lineLen / lightcolors.size();
        float startLocx = - lineLen / 2.0f;
        for (size_t i = 0; i < lightcolors.size(); i++)
        {
            auto pointLight = NjmGameObj::createPointLight(0.3f);
            pointLight.color = lightcolors[i];
            pointLight.transform.translation = {
                startLocx + (float)i * step,
                -0.5f,
                1.0f,
            };
            pointLight.pointLight->lightIntensity = (float)(i + 1) * 1.0f / (float)lightcolors.size();
            gameObjs.emplace(pointLight.getId(), std::move(pointLight));
        }
        
    }

    void FirstApp::loadImgs()
    {
        const std::string& IMG_BASE_PATH = "src/texs/";

        std::string imgName = "eg1.png";
        std::string imgPath = IMG_BASE_PATH + imgName;
        std::string absoluteImgPath = njm::getCompletePath(imgPath);
        // NjmImg img1{imgName, absoluteImgPath};
        auto &img1 = std::make_shared<NjmImg>(imgName, absoluteImgPath);
        imgIds.push_back(imgName);
        njmImgs.emplace(imgName, std::move(img1));
    }

    void FirstApp::printAssetsInfo()
    {
        std::cout << "--FirstApp::printAssetsInfo--" << std::endl;
        std::cout << "njmImgs.size(): " << njmImgs.size() << std::endl;
        std::cout << "imgIds.size(): " << imgIds.size() << std::endl;
        for (auto &imgId : imgIds)
        {
            std::cout << "  " << imgId << std::endl;
        }

        std::cout << "njmGameObjs.size(): " << gameObjs.size() << std::endl;
        std::cout << "njmGameObjs:" << std::endl;
        for (auto &gameObj : gameObjs)
        {
            std::cout << "  " << gameObj.first << std::endl;
        }
    }
}